/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package apoc.convert;

import apoc.coll.SetBackedList;
import apoc.meta.Meta.Types;
import apoc.util.Util;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.neo4j.graphdb.Entity;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.internal.helpers.collection.Iterators;
import org.neo4j.logging.Log;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Description;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.UserFunction;

/**
 * @author mh
 * @since 29.05.16
 */
public class Convert {

    @Context
    public Log log;

    @UserFunction
    @Description("apoc.convert.toMap(value) | tries it's best to convert the value to a map")
    public Map<String, Object> toMap(@Name("map") Object map) {

        if (map instanceof Entity) {
            return ((Entity) map).getAllProperties();
        } else if (map instanceof Map) {
            return (Map<String, Object>) map;
        } else {
            return null;
        }
    }

    @UserFunction(
            deprecatedBy =
                    "Neo4j toString(value) / toStringOrNull(value) functions. This function will be removed in 5.0")
    @Description("apoc.convert.toString(value) | tries it's best to convert the value to a string")
    @Deprecated
    public String toString(@Name("string") Object string) {
        return string == null ? null : string.toString();
    }

    @UserFunction
    @Description("apoc.convert.toList(value) | tries it's best to convert the value to a list")
    public List<Object> toList(@Name("list") Object list) {
        return convertToList(list);
    }

    @UserFunction(
            deprecatedBy =
                    "Neo4j toBoolean(value) / toBooleanOrNull(value) functions. This function will be removed in 5.0")
    @Description("apoc.convert.toBoolean(value) | tries it's best to convert the value to a boolean")
    @Deprecated
    public Boolean toBoolean(@Name("bool") Object bool) {
        return Util.toBoolean(bool);
    }

    @UserFunction
    @Description("apoc.convert.toNode(value) | tries it's best to convert the value to a node")
    public Node toNode(@Name("node") Object node) {
        return node instanceof Node ? (Node) node : null;
    }

    @UserFunction
    @Description("apoc.convert.toRelationship(value) | tries it's best to convert the value to a relationship")
    public Relationship toRelationship(@Name("relationship") Object relationship) {
        return relationship instanceof Relationship ? (Relationship) relationship : null;
    }

    @SuppressWarnings("unchecked")
    public static List convertToList(Object list) {
        if (list == null) return null;
        else if (list instanceof List) return (List) list;
        else if (list instanceof Collection) return new ArrayList((Collection) list);
        else if (list instanceof Iterable)
            return Iterators.addToCollection(((Iterable) list).iterator(), (List) new ArrayList<>(100));
        else if (list instanceof Iterator)
            return Iterators.addToCollection((Iterator) list, (List) new ArrayList<>(100));
        else if (list.getClass().isArray()) {
            return convertArrayToList(list);
        }
        return Collections.singletonList(list);
    }

    public static List convertArrayToList(Object list) {
        final Object[] objectArray;
        if (list.getClass().getComponentType().isPrimitive()) {
            int length = Array.getLength(list);
            objectArray = new Object[length];
            for (int i = 0; i < length; i++) {
                objectArray[i] = Array.get(list, i);
            }
        } else {
            objectArray = (Object[]) list;
        }
        List result = new ArrayList<>(objectArray.length);
        Collections.addAll(result, objectArray);
        return result;
    }

    @SuppressWarnings("unchecked")
    private <T> List<T> convertToList(Object list, Class<T> type) {
        List<Object> convertedList = convertToList(list);
        if (convertedList == null) {
            return null;
        }
        Stream<T> stream = null;
        Types varType = Types.of(type);
        switch (varType) {
            case INTEGER:
                stream = (Stream<T>) convertedList.stream().map(Util::toLong);
                break;
            case FLOAT:
                stream = (Stream<T>) convertedList.stream().map(Util::toDouble);
                break;
            case STRING:
                stream = (Stream<T>) convertedList.stream().map(this::toString);
                break;
            case BOOLEAN:
                stream = (Stream<T>) convertedList.stream().map(this::toBoolean);
                break;
            case NODE:
                stream = (Stream<T>) convertedList.stream().map(this::toNode);
                break;
            case RELATIONSHIP:
                stream = (Stream<T>) convertedList.stream().map(this::toRelationship);
                break;
            default:
                throw new RuntimeException("Supported types are: Integer, Float, String, Boolean, Node, Relationship");
        }
        return stream.collect(Collectors.toList());
    }

    @SuppressWarnings("unchecked")
    @UserFunction
    @Description("apoc.convert.toSet(value) | tries it's best to convert the value to a set")
    public List<Object> toSet(@Name("list") Object value) {
        List list = convertToList(value);
        return list == null ? null : new SetBackedList(new LinkedHashSet<>(list));
    }

    @UserFunction(deprecatedBy = "Neo4j toIntegerList(value) function. This function will be removed in 5.0")
    @Description("apoc.convert.toIntList(value) | tries it's best to convert " + "the value to a list of integers")
    @Deprecated
    public List<Long> toIntList(@Name("list") Object list) {
        return convertToList(list, Long.class);
    }

    @UserFunction(deprecatedBy = "Neo4j toStringList(value) function. This function will be removed in 5.0")
    @Description("apoc.convert.toStringList(value) | tries it's best to convert " + "the value to a list of strings")
    @Deprecated
    public List<String> toStringList(@Name("list") Object list) {
        return convertToList(list, String.class);
    }

    @UserFunction(deprecatedBy = "Neo4j toBooleanList(value) function. This function will be removed in 5.0")
    @Description("apoc.convert.toBooleanList(value) | tries it's best to convert " + "the value to a list of booleans")
    @Deprecated
    public List<Boolean> toBooleanList(@Name("list") Object list) {
        return convertToList(list, Boolean.class);
    }

    @UserFunction
    @Description("apoc.convert.toNodeList(value) | tries it's best to convert " + "the value to a list of nodes")
    public List<Node> toNodeList(@Name("list") Object list) {
        return convertToList(list, Node.class);
    }

    @UserFunction
    @Description("apoc.convert.toRelationshipList(value) | tries it's best to convert "
            + "the value to a list of relationships")
    public List<Relationship> toRelationshipList(@Name("list") Object list) {
        return convertToList(list, Relationship.class);
    }

    @UserFunction(
            deprecatedBy =
                    "Neo4j toInteger(value) / toIntegerOrNull(value) functions. This function will be removed in 5.0")
    @Description("apoc.convert.toInteger(value) | tries it's best to convert the value to an integer")
    @Deprecated
    public Long toInteger(@Name("object") Object obj) {
        if (obj == null || obj.equals("")) {
            return null;
        }

        Types varType = Types.of(obj);
        switch (varType) {
            case INTEGER:
            case FLOAT:
                return ((Number) obj).longValue();
            case STRING:
                return parseLongString((String) obj);
            case BOOLEAN:
                return ((boolean) obj) ? 1L : 0L;
            default:
                return null;
        }
    }

    private Long parseLongString(String input) {
        if (input.equalsIgnoreCase("true")) {
            return 1L;
        }
        if (input.equalsIgnoreCase("false")) {
            return 0L;
        }
        if (input.startsWith("0x")) {
            return Long.valueOf(input.substring(2), 16);
        }
        try {
            return Long.parseLong(input);
        } catch (NumberFormatException nfe) {
            try {
                return (long) Double.parseDouble(input);
            } catch (NumberFormatException nfe2) {
                // String was not able to be parsed, return null
                return null;
            }
        }
    }

    @UserFunction(
            deprecatedBy =
                    "Neo4j toFloat(value) / toFloatOrNull(value) functions. This function will be removed in 5.0")
    @Description("apoc.convert.toFloat(value) | tries it's best to convert the value to a float")
    @Deprecated
    public Double toFloat(@Name("object") Object obj) {
        if (obj == null || obj.equals("")) {
            return null;
        }

        Types varType = Types.of(obj);
        switch (varType) {
            case FLOAT:
            case INTEGER:
                return ((Number) obj).doubleValue();
            case STRING:
                return parseDoubleString((String) obj);
            case BOOLEAN:
                return ((boolean) obj) ? 1D : 0D;
            default:
                return null;
        }
    }

    private Double parseDoubleString(String input) {
        if (input.equalsIgnoreCase("true")) {
            return 1D;
        }
        if (input.equalsIgnoreCase("false")) {
            return 0D;
        }
        try {
            if (input.startsWith("0x")) {
                return (double) Long.parseLong(input.substring(2), 16);
            }
            return Double.parseDouble(input);
        } catch (NumberFormatException ex) {
            return null;
        }
    }
}
